/************************************
*  Copyright (C) 2010 Julien MOTTIN.
*  Contact: julien.mottin@gueydan.eu
*
*  This file is part of Gueydan.
*
*  Gueydan is free software: you can redistribute it and/or modify
*  it under the terms of the GNU General Public License as published by
*  the Free Software Foundation, either version 3 of the License, or
*  (at your option) any later version.
*
*  Gueydan is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*  GNU General Public License for more details.
*
*  You should have received a copy of the GNU General Public License
*  along with Gueydan.  If not, see <http://www.gnu.org/licenses/>.
*
*************************************/
// Local
#include "recipe.h"
#include "util.h"
#include "hop.h"
#include "fermentable.h"
#include "yeast.h"
#include "xmldico.h"

// QtCore
#include <QtGlobal>

// QtXml
#include <QDomNode>
#include <QDomElement>
#include <QDomDocument>
#include <QDomNodeList>
#include <QDomText>

// Standard library
#include <math.h>

Recipe::Recipe(const QString &iName, const int iVersion, 
               const RecipeType &iType, 
               const Style *ipStyle,
               const QString &iBrewer, 
               const Volume &iBatchSize, 
               const Volume &iBoilSize, 
               const Duration &iBoilTime, 
               float iEfficiency)
:BeerXMLObject(iName,iVersion)
,_Type(iType)
,_hStyle(ipStyle)
,_BrewerName(iBrewer)
,_BatchSize(iBatchSize)
,_BoilSize(iBoilSize)
,_BoilTime(iBoilTime)
,_Efficiency(iEfficiency)
,_Notes()
,_TasteNotes()
,_OG(-1000)
,_FG(-1000)
,_NbStages(-1000)
,_OGEstimate(-1000)
,_FGEstimate(-1000)
,_ABV(-1000)
,_AAU(-1000)
,_ColorEstimate()
,_Modified(false)
,_NeedRecalc(true)
{
    _Hops.clear();
    _Ferms.clear();
    _Yeasts.clear();
}

Recipe::Recipe()
:BeerXMLObject()
,_Type(RTAllGrain)
,_BrewerName("Gueydan")
,_BatchSize(Volume(14,Liter))
,_BoilSize(Volume(25,Liter))
,_BoilTime(Duration(1,Hour))
,_Efficiency(75.0f)
,_TasteNotes()
,_OG(-1000)
,_FG(-1000)
,_NbStages(-1000)
,_OGEstimate(-1000)
,_FGEstimate(-1000)
,_ABV(-1000)
,_AAU(-1000)
,_ColorEstimate()
,_Modified(false)
,_NeedRecalc(true)
{
    _Hops.clear();
    _Ferms.clear();
    _Yeasts.clear();
}

Recipe::~Recipe()
{
    _Hops.clear();
    _Ferms.clear();
    _Yeasts.clear();
}

bool Recipe::operator==(const Recipe &iOther) const
{
    return ((*this).BeerXMLObject::operator==(iOther) &&
            _Type == iOther._Type &&
            _hStyle == iOther._hStyle &&
            _BrewerName == iOther._BrewerName &&
            _BatchSize == iOther._BatchSize &&
            _BoilSize == iOther._BoilSize &&
            ComparePrecision(_Efficiency,iOther._Efficiency,0.05) &&
            _Notes == iOther._Notes &&
            _TasteNotes == iOther._TasteNotes &&
            _OG == iOther._OG &&
            _FG == iOther._FG &&
            _NbStages == iOther._NbStages &&
            _Hops == iOther._Hops &&
            _Ferms == iOther._Ferms &&
            _Yeasts == iOther._Yeasts &&
            _hMash == iOther._hMash);
}

bool Recipe::operator!=(const Recipe &iOther) const
{
    return (!((*this)==iOther));
}

bool Recipe::operator==(const BeerXMLObject &iOther) const
{
    bool res = false;
    BeerXMLHandler hObj(&iOther);
    Recipe *pRecipe = (Recipe*)hObj;
    if (pRecipe)
    {
        res = (*this)==(*pRecipe);
    }
    return res;
}


bool Recipe::operator!=(const BeerXMLObject &iOther) const
{
    return (!((*this)==iOther));
}


int Recipe::CreateFromXMLNode(const QDomNode &iNode)
{
    int oRC = 1;
    if (!iNode.isNull())
    {
        QDomElement Element;

        // Case Document : check if doc has only one recipe element
        if (iNode.isDocument())
        {
            QDomDocument Doc = iNode.toDocument();
            QDomNodeList List = Doc.elementsByTagName(RECIPE);
            if (List.length()==1)
                Element = List.item(0).toElement();
        }
        // Default : cast to element
        else
            Element = iNode.toElement();

        // Check element consistency
        if (!Element.isNull() && RECIPE==Element.tagName())
        {
            bool Name = false;
            bool Version = false;
            bool Type = false;
            bool StyleChk = false;
            bool Brewer = false;
            bool BatchSize = false;
            bool BoilSize = false;
            bool BoilTime = false;
            bool Efficiency = false;
            bool Hops = false;
            bool Fermentables = false;
            bool Yeasts = false;
            QDomNode node = Element.firstChild();
            while (!node.isNull())
            {
                if (node.isElement())
                {
                    QDomElement elem = node.toElement();
                    QString name = elem.tagName();
                    QString text = elem.text();
                    if (name==RECIPE_NAME)
                    {
                        _Name = text;
                        Name = true;
                    }
                    else if (name==RECIPE_VERSION)
                    {
                       bool ok = false;
                       int v = text.toInt(&ok);
                       if (ok)
                       {
                           _Version = v;
                           Version = true;
                       }
                    }
                    else if (name==RECIPE_STYLE)
                    {
                        Style *pStyle = new Style();
                        Q_ASSERT(0==pStyle->CreateFromXMLNode(elem));
                        {
                            _hStyle = pStyle;
                            StyleChk = true;
                        }

                        // Clean
                        pStyle->Release();
                    }
                    else if (name==RECIPE_TYPE)
                    {
                        Type = true;
                        QString str = text.toLower();
                        if (str==RECIPE_TYPE_EXTRACT.toLower())
                            _Type = RTExtract;
                        else if (str==RECIPE_TYPE_PARTIALMASH.toLower())
                            _Type = RTPartialMash;
                        else if (str==RECIPE_TYPE_ALLGRAIN.toLower())
                            _Type = RTAllGrain;
                        else
                        {
                            Type = false;
                            break;
                        }
                    }
                    else if (name==RECIPE_BREWER)
                    {
                        _BrewerName = text;
                        Brewer = true;
                    }
                    else if (name==RECIPE_BATCHSIZE)
                    {
                        bool ok = false;
                        _BatchSize.FromString(text,&ok);
                        if (ok)
                            BatchSize = true;
                    }
                    else if (name==RECIPE_BOILSIZE)
                    {
                        bool ok = false;
                        _BoilSize.FromString(text,&ok);
                        if (ok)
                            BoilSize = true;
                    }
                    else if (name==RECIPE_BOILTIME)
                    {
                        bool ok = false;
                        _BoilTime.FromString(text,&ok);
                        if (ok)
                            BoilTime = true;
                    }
                    else if (name==RECIPE_EFFICIENCY)
                    {
                        bool ok= false;
                        float v = text.toFloat(&ok);
                        if (ok)
                        {
                            _Efficiency = v;
                            Efficiency = true;
                        }
                    }
                    else if (name==RECIPE_HOPS)
                    {
                        Hops = ParseBeerXML(node,HOP,&_Hops);
                    }
                    else if (name==RECIPE_FERMENTABLES)
                    {
                        Fermentables = ParseBeerXML(node,FERMENTABLE,&_Ferms);
                    }
                    else if (name==RECIPE_YEASTS)
                    {
                        Yeasts = ParseBeerXML(node,YEAST,&_Yeasts);
                    }
                    else if (name==RECIPE_MASH)
                    {
                        _hMash.CreateFromXMLNode(node);
                    }
                    else if (name==RECIPE_NOTES)
                    {
                        _Notes = text;
                    }
                    else if (name==RECIPE_TASTENOTES)
                    {
                        _TasteNotes = text;
                    }
                    else if (name==RECIPE_OG)
                    {
                        bool ok = false;
                        double v = text.toDouble(&ok);
                        if (ok)
                            _OG = v;
                    }
                    else if (name==RECIPE_FG)
                    {                        
                        bool ok = false;
                        double v = text.toDouble(&ok);
                        if (ok)
                            _FG = v;
                    }
                    else if (name==RECIPE_FERMSTAGES)
                    {
                        bool ok = false;
                        int v = text.toInt(&ok);
                        if (ok)
                            _NbStages = v;
                    }
                    else if (name==RECIPE_PRIMAGE)
                    {

                    }
                    else if (name==RECIPE_PRIMTEMP)
                    {

                    }
                    else if (name==RECIPE_SECONDAGE)
                    {

                    }
                    else if (name==RECIPE_SECONDTEMP)
                    {

                    }
                    else if (name==RECIPE_TERTAGE)
                    {

                    }
                    else if (name==RECIPE_TERTTEMP)
                    {

                    }
                    else if (name==RECIPE_AGE)
                    {

                    }
                    else if (name==RECIPE_AGETEMP)
                    {

                    }
                    else if (name==RECIPE_DATE)
                    {

                    }
                    else if (name==RECIPE_PRIMSUGARNAME)
                    {

                    }
                    else if (name==RECIPE_PRIMSUGAREQUIV)
                    {

                    }
                }

                // iterate
                node = node.nextSibling();
            }

            // Check & update output
            if (Name && Version && Type && StyleChk && Brewer && BatchSize &&
                BoilSize && BoilTime && Efficiency && Hops && Fermentables && Yeasts)
            {
                // Success
                oRC = 0;
                Recalc();
            }
        }
    }
    return oRC;
}

int Recipe::ToXMLNode(QDomNode &iParentNode) const
{
    int oRC = 1;

    // First, create valid element node
    QDomDocument doc = iParentNode.ownerDocument();
    QDomElement RecipeNode = doc.createElement(RECIPE);
    if (!RecipeNode.isNull())
    {
        if (!_Name.isEmpty() && _Version>0 &&
            !!_hStyle && _BatchSize.Value(Liter)>0.01 && _BoilSize.Value(Liter)>0.01 &&
            _BoilTime.Value(Minute)>0 && _Efficiency>0)
        {
            // Name
            QDomElement Node = doc.createElement(RECIPE_NAME);
            Q_ASSERT(!Node.isNull());
            QDomText Text = doc.createTextNode(_Name);
            Q_ASSERT(!Text.isNull());
            Node.appendChild(Text);
            RecipeNode.appendChild(Node);

            // Version
            Node = doc.createElement(RECIPE_VERSION);
            Q_ASSERT(!Node.isNull());
            Text = doc.createTextNode(QString::number(_Version));
            Q_ASSERT(!Text.isNull());
            Node.appendChild(Text);
            RecipeNode.appendChild(Node);

            QString str;
            switch (_Type)
            {
            case RTExtract :
                {
                    str = RECIPE_TYPE_EXTRACT;
                    break;
                }
            case RTPartialMash :
                {
                    str = RECIPE_TYPE_PARTIALMASH;
                    break;
                }
            case RTAllGrain :
                {
                    str = RECIPE_TYPE_ALLGRAIN;
                    break;
                }
            }

            // Type
            Node = doc.createElement(RECIPE_TYPE);
            Q_ASSERT(!Node.isNull());
            Text = doc.createTextNode(str);
            Q_ASSERT(!Text.isNull());
            Node.appendChild(Text);
            RecipeNode.appendChild(Node);

            // Style
            Q_ASSERT(0==_hStyle->ToXMLNode(RecipeNode));

            // Brewer
            Node = doc.createElement(RECIPE_BREWER);
            Q_ASSERT(!Node.isNull());
            Text = doc.createTextNode(_BrewerName);
            Q_ASSERT(!Text.isNull());
            Node.appendChild(Text);
            RecipeNode.appendChild(Node);

            // Batch Size
            Node = doc.createElement(RECIPE_BATCHSIZE);
            Q_ASSERT(!Node.isNull());
            Text = doc.createTextNode(QString::number(_BatchSize.Value(Liter)));
            Q_ASSERT(!Text.isNull());
            Node.appendChild(Text);
            RecipeNode.appendChild(Node);

            // Boil Size
            Node = doc.createElement(RECIPE_BOILSIZE);
            Q_ASSERT(!Node.isNull());
            Text = doc.createTextNode(QString::number(_BoilSize.Value(Liter)));
            Q_ASSERT(!Text.isNull());
            Node.appendChild(Text);
            RecipeNode.appendChild(Node);

            // Boil Time
            Node = doc.createElement(RECIPE_BOILTIME);
            Q_ASSERT(!Node.isNull());
            Text = doc.createTextNode(QString::number(_BoilTime.Value(Minute)));
            Q_ASSERT(!Text.isNull());
            Node.appendChild(Text);
            RecipeNode.appendChild(Node);

            // Efficiency
            Node = doc.createElement(RECIPE_EFFICIENCY);
            Q_ASSERT(!Node.isNull());
            Text = doc.createTextNode(QString::number(_Efficiency));
            Q_ASSERT(!Text.isNull());
            Node.appendChild(Text);
            RecipeNode.appendChild(Node);

            // Hops
            int i = 0;
            Node = doc.createElement(RECIPE_HOPS);
            Q_ASSERT(!Node.isNull());
            for (i=0;i<_Hops.size();i++)
                Q_ASSERT(0==_Hops[i]->ToXMLNode(Node));
            RecipeNode.appendChild(Node);

            // Fermentables
            Node = doc.createElement(RECIPE_FERMENTABLES);
            for (i=0;i<_Ferms.size();i++)
                Q_ASSERT(0==_Ferms[i]->ToXMLNode(Node));
            RecipeNode.appendChild(Node);

            // Yeasts
            Node = doc.createElement(RECIPE_YEASTS);
            for (i=0;i<_Yeasts.size();i++)
                Q_ASSERT(0==_Yeasts[i]->ToXMLNode(Node));
            RecipeNode.appendChild(Node);

            // Notes
            if (!_Notes.isNull())
            {
                Node = doc.createElement(RECIPE_NOTES);
                Q_ASSERT(!Node.isNull());
                Text = doc.createTextNode(_Notes);
                Q_ASSERT(!Text.isNull());
                Node.appendChild(Text);
                RecipeNode.appendChild(Node);
            }

            // Taste notes
            if (!_Notes.isNull())
            {
                Node = doc.createElement(RECIPE_TASTENOTES);
                Q_ASSERT(!Node.isNull());
                Text = doc.createTextNode(_TasteNotes);
                Q_ASSERT(!Text.isNull());
                Node.appendChild(Text);
                RecipeNode.appendChild(Node);
            }

            // OG
            if (_OG>0)
            {
                Node = doc.createElement(RECIPE_OG);
                Q_ASSERT(!Node.isNull());
                Text = doc.createTextNode(QString::number(_OG));
                Q_ASSERT(!Text.isNull());
                Node.appendChild(Text);
                RecipeNode.appendChild(Node);
            }

            // FG
            if (_FG>0)
            {
                Node = doc.createElement(RECIPE_FG);
                Q_ASSERT(!Node.isNull());
                Text = doc.createTextNode(QString::number(_FG));
                Q_ASSERT(!Text.isNull());
                Node.appendChild(Text);
                RecipeNode.appendChild(Node);
            }

            // Update output
            QDomNode zeNode = iParentNode.appendChild(RecipeNode);
            if (!zeNode.isNull())
                oRC = 0;
        }
    }

    return oRC;
}

void Recipe::AddHop(Hop *ipHop)
{
    BeerXMLHandler hHop = ipHop;
    _Hops.append(hHop);
    _Modified = true;
    _NeedRecalc = true;
}

int Recipe::NbHops() const
{
    return _Hops.size();
}

const Hop * Recipe::GetHop(const int iHopIdx) const
{
    return _Hops[iHopIdx];
}

void Recipe::AddFerm(Fermentable *ipFerm)
{
    BeerXMLHandler hFerm = ipFerm;
    _Ferms.append(hFerm);
    _Modified = true;
    _NeedRecalc = true;
}

int Recipe::NbFerms() const
{
    return _Ferms.size();
}

const Fermentable * Recipe::GetFerm(const int iFermIdx) const
{
    return _Ferms[iFermIdx];
}

void Recipe::AddYeast(Yeast *ipYeast)
{
    BeerXMLHandler hYeast = ipYeast;
    _Yeasts.append(hYeast);
    _Modified = true;
    _NeedRecalc = true;
}

int Recipe::NbYeasts() const
{
    return _Yeasts.size();
}

const Yeast * Recipe::GetYeast(int iYeastIdx) const
{
    return _Yeasts[iYeastIdx];
}

double Recipe::HopUtilization(HopUtilizationMethod iMethod) const
{
    double oUtil = 0;
    switch (iMethod)
    {
    case Rager:
        {
            double GA = 0;

            // Rager factor
            double Rag = 18.11 + 13.86*tanh((_BoilTime.Value(Minute)-31.32)/18.27);

            // Gravity adjustment
            double BoilGravity = OriginalGravity();
            if (BoilGravity>1.05)
                GA = 5*(BoilGravity - 1.05);

            // Utilization
            oUtil = Rag/(1+GA);
            break;
        }
    case Tinseth:
        {
            // Bigness factor
            double Big = 1.65 * pow(0.000125,OriginalGravity()-1);

            // Boil time factor
            double Boil = 0.241*(1 - exp(-0.04*_BoilTime.Value(Minute)));

            // Utilization
            oUtil = Big*Boil;
            break;
        }
    }
    return oUtil;
}

double Recipe::AAU() const
{
    if (_NeedRecalc)
        Recalc();

    return _AAU;
}

double Recipe::IBU(HopUtilizationMethod iMethod) const
{
    double oRes = 0;

    // Calcul Hop IBU
    oRes = AAU() * HopUtilization(iMethod) / _BatchSize.Value(Liter);

    return oRes;
}

double Recipe::OriginalGravity() const
{
    if (_OG>0)
        return _OG;
    else
    {
        if (_NeedRecalc)
            Recalc();

        return _OGEstimate;
    }
}

double Recipe::FinalGravity() const
{
    if (_FG>0)
        return _FG;
    else
    {
        if (_NeedRecalc)
            Recalc();

        return _FGEstimate;
    }
}

double Recipe::ABV() const
{
    if (_NeedRecalc)
        Recalc();

    return _ABV;
}

Color Recipe::ColorEstimate() const
{
    if (_NeedRecalc)
        Recalc();

    return _ColorEstimate;
}

bool Recipe::ParseBeerXML(const QDomNode &iFather, const QString &iTag, QList<BeerXMLHandler>* pList)
{
    bool found = false;
    if (pList)
    {
        QDomNode node = iFather.firstChild();
        while (!node.isNull())
        {
            if (node.isElement() && node.nodeName()==iTag)
            {
                // instantiate new xmlObj & parse
                BeerXMLHandler hObj;
                int rc = hObj.CreateFromXMLNode(node);
                if (0==rc)
                {
                    pList->append(hObj);
                    found = true;
                }
            }

            // Iterate
            node = node.nextSibling();
        }
    }

    return found;
}

void Recipe::Modify()
{
    _Modified = true;
    emit Modified(this);
}

void Recipe::Recalc() const
{
    /*
    * Original Gravity
    */

    // Points of density of sugar per Kg per L
    const double Sugar = 386.5;
    int i=0;

    double tmp = 0;
    int NbFerms = _Ferms.size();
    for (i=0;i<NbFerms;i++)
    {
        Fermentable *pFerm = (Fermentable*)_Ferms[i];
        if (pFerm)
        {
            // 0.01 (percent) * Sugar * Yield * Amount
            double DensityContrib = (0.01*Sugar*pFerm->_Yield*pFerm->_Amount.Value(Kilogram));

            // For grains & adjuncts apply efficiency factor
            if (FTGrain==pFerm->_Type || FTAdjunct==pFerm->_Type)
                DensityContrib *= 0.01*_Efficiency;

            tmp += DensityContrib;
        }
    }

    // Now divide by the wort volume
    Q_ASSERT(fabs(_BatchSize.Value(Liter))>0.001);
    tmp /= _BatchSize.Value(Liter);

    // Now put in on canonical form : 1.xxx
    tmp = 1 + 0.001*tmp;

    // Store OG
    _OGEstimate = tmp;
    double tmpOG = _OG>0?_OG:_OGEstimate;

    /*
    * Final Gravity
    */

    // Compute yeast attenuation
    double Attenuation = 75.0;
    int NbYeasts = _Yeasts.size();
    int count = 0;
    tmp = 0;
    for (i=0;i<NbYeasts;i++)
    {
        Yeast *pYeast = (Yeast*)_Yeasts[i];
        if (pYeast)
        {
            if (pYeast->_Attenuation>10)
            {
                count++;
                tmp +=  pYeast->_Attenuation;
            }
        }
    }

    // update attenuation value
    if (count>0)
        Attenuation = tmp/count;

    _FGEstimate = (tmpOG-1)*0.01*(100-Attenuation)+1;
    double tmpFG = _FG>0?_FG:_FGEstimate;

    /*
    * Final Gravity
    */
    _ABV = 1.32*(tmpOG-tmpFG)+0.004;

    /*
    * AAU
    */
    tmp = 0;
    int NbHops = _Hops.size();
    for (i=0;i<NbHops;i++)
    {
        Hop *pHop = (Hop*)_Hops[i];
        if (pHop)
        {
            if (HUBoil==pHop->_Use)
                tmp += pHop->_Alpha*pHop->_Amount.Value(Gram);
        }
    }

    // Multiply by 1000 to have AAU in mg
    // Divide by 100 to have _Alpha value (instead of percentage)
    // => multiply by 10
    _AAU = tmp*10;

    /*
    * Color estimation : apply Morey law
    * SRM color = 1.4922 * (MCU ** 0.6859)  (MCU is in LB per Gal)
    */
    tmp = 0;
    for (i=0;i<NbFerms;i++)
    {
        Fermentable *pFerm = (Fermentable*)_Ferms[i];
        if (pFerm)
            // Add MCU contribution
            tmp += (pFerm->_Color.Value(SRM)*pFerm->_Amount.Value(Pound));
    }

    // Divide by batch volume in Gallon
    tmp/=_BatchSize.Value(Gallon);
    double srmColor = 1.4922*pow(tmp,0.6859);
    _ColorEstimate = Color(srmColor,SRM);

    // Update
    _NeedRecalc = false;
}
